package com.motorola.demomode.facedetect;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import android.app.Service;
import android.content.ActivityNotFoundException;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.RectF;
import android.hardware.Camera;
import android.hardware.Camera.Face;
import android.hardware.Camera.FaceDetectionListener;
import android.hardware.Camera.Size;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.util.Log;
import android.view.Gravity;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.Toast;


public class FaceDetectService extends Service {
    private static final String TAG = "FaceDetectService";

    // android.hardware.Camera.startFaceDetection()
    static final int BUILTIN_DETECTOR = 0;
    // capture preview frames and use FaceDetector
    static final int CUSTOM_WITH_FACE_DETECTOR = 1;

    static int mFaceDetectMethod = CUSTOM_WITH_FACE_DETECTOR;

    public static final String ACTION_FACE_DETECT_SERVICE_START =
        "com.motorola.demomode.facedetect.intent.action.FACE_DETECT_SERVICE_START";
    public static final String ACTION_FACE_DETECT_SERVICE_STOP =
        "com.motorola.demomode.facedetect.intent.action.FACE_DETECT_SERVICE_STOP";
    public static final String ACTION_FACE_DETECT_TO_FOREGROUND =
        "com.motorola.demomode.facedetect.intent.action.FACE_DETECT_TO_FOREGROUND";
    public static final String ACTION_FACE_DETECT_TO_BACKGROUND =
        "com.motorola.demomode.facedetect.intent.action.FACE_DETECT_TO_BACKGROUND";
    public static final String ACTION_FACE_DETECT_SWITCH_CAM =
        "com.motorola.demomode.facedetect.intent.action.FACE_DETECT_SWITCH_CAM";

    private static final int BG_PREVIEW_WINDOW_WIDTH = 200;
    private static final int BG_PREVIEW_WINDOW_HEIGHT = 150;

    private static final String BCAST_CONFIGCHANGED = "android.intent.action.CONFIGURATION_CHANGED";

    private long SESSION_ID;

    static FaceDetectService mThis;
    private CameraPreview mPreview;
    private DrawOnTop mTopView;

    WindowManager mWindowManager;
    private WindowManager.LayoutParams mParams = new WindowManager.LayoutParams();

    int mCameraCurrentlyLocked;
    private Camera mCamera;
    private boolean mIsForeground;

    private static final int MSG_UPDATE_STATUS = 1;
    private static final int MSG_GET_PREVIEW = 1;

    private Handler mHandler = new MyHandler();

    Camera.PreviewCallback mPreviewCallback = new Camera.PreviewCallback() {
        @Override
        public void onPreviewFrame(byte[] data, Camera camera) {
            if (FaceDetectService.mThis == null) {
                return;
            }
            FaceDetectService.mThis.mHandler.sendEmptyMessageDelayed(MSG_GET_PREVIEW, 1000);
            mCamera.setOneShotPreviewCallback(mPreviewCallback);
        }
    };

    BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            if (action == null) {
                return;
            }
            if ( action.equals( BCAST_CONFIGCHANGED ) ) {
                if (mPreview != null) {
                    mPreview.setCameraDisplayOrientation();
                }
            }
        }
    };

    /** A safe way to get an instance of the Camera object. */
    public Camera getCameraInstance() {
        Camera c = null;
        c = Camera.open(mCameraCurrentlyLocked);
        return c; // returns null if camera is unavailable
    }

    @Override
    public void onCreate() {
        super.onCreate();
        Log.d(TAG, "onCreate start");
        SESSION_ID = System.currentTimeMillis();
        mWindowManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE);

        IntentFilter filter = new IntentFilter();
        filter.addAction(BCAST_CONFIGCHANGED);
        registerReceiver(mBroadcastReceiver, filter);

        mThis = this;
    }

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        super.onStartCommand(intent, flags, startId);
        handleCommand(intent);

        return START_STICKY;
    }

    void handleCommand(Intent intent) {
        if (null == intent) {
            return;
        }

        String action = intent.getAction();
        if (null == action) {
            return;
        }

        if (action.equals(ACTION_FACE_DETECT_SERVICE_START)) {
            // Create an instance of Camera
            if (mCamera == null) {
                boolean front = intent.getBooleanExtra("isFront", true);
                mCameraCurrentlyLocked = front ? 1 : 0;

                mCamera = getCameraInstance();
            }

            // insert camera preview
            if (mPreview == null) {
                mPreview = new CameraPreview(this);
                mPreview.setCamera(mCamera);

                mParams.type = WindowManager.LayoutParams.TYPE_PHONE;
                mParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
                        | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
                mParams.format = PixelFormat.TRANSLUCENT;
                mParams.gravity = Gravity.LEFT | Gravity.TOP;
                mParams.width = BG_PREVIEW_WINDOW_WIDTH;
                mParams.height = BG_PREVIEW_WINDOW_HEIGHT;

                mWindowManager.addView(mPreview, mParams);
            }
            if (mTopView == null) {
                mTopView = new DrawOnTop(this);
                mWindowManager.addView(mTopView, mParams);
            }
        } else if (action.equals(ACTION_FACE_DETECT_SERVICE_STOP)) {
            if (mPreview != null) {
                mPreview.stopFaceDetection();
                mPreview.setCamera(null);
                mWindowManager.removeView(mPreview);
                mPreview = null;
            }
            if (mTopView != null) {
                mWindowManager.removeView(mTopView);
                mTopView = null;
            }
            releaseCamera(); // release the camera immediately on pause event
            //stopSelf();
        } else if (action.equals(ACTION_FACE_DETECT_TO_FOREGROUND)) {
            if (mPreview != null) {
                foregroundDisplay();
                mPreview.setCameraDisplayOrientation();
                cancelToast();
            }
        } else if (action.equals(ACTION_FACE_DETECT_TO_BACKGROUND)) {
            if (mPreview != null) {
                backgroundDisplay();
                mPreview.setCameraDisplayOrientation();
                cancelToast();
                showToast("Face Detection in background mode. Re-launch the App back to foreground.");
            }
        } else if (action.equals(ACTION_FACE_DETECT_SWITCH_CAM)) {
            if (mPreview != null) {
                // OK, we have multiple cameras.
                // Release this camera -> cameraCurrentlyLocked
                if (mCamera != null) {
                    mPreview.stopFaceDetection();
                    mPreview.setCamera(null);
                    mCamera.stopPreview();
                    mCamera.release();
                    mCamera = null;
                }

                // Acquire the next camera and request Preview to reconfigure
                // parameters.
                mCameraCurrentlyLocked = (mCameraCurrentlyLocked == 1) ? 0:1;
                mCamera = Camera.open(mCameraCurrentlyLocked);
                mPreview.switchCamera(mCamera);

                // Start the preview
                mCamera.startPreview();
                mPreview.startFaceDetection();
            }
        }
    }

    @Override
    public void onDestroy() {
        mThis = null;
        if (mPreview != null) {
            mPreview.stopFaceDetection();
            mWindowManager.removeView(mPreview);
            mPreview.setCamera(null);
            mPreview = null;
        }
        if (mTopView != null) {
            mWindowManager.removeView(mTopView);
            mTopView = null;
        }
        releaseCamera();
        if (mBroadcastReceiver != null) {
            unregisterReceiver(mBroadcastReceiver);
            mBroadcastReceiver = null;
        }
    }

    void foregroundDisplay() {
        mParams.width = WindowManager.LayoutParams.MATCH_PARENT;
        mParams.height = WindowManager.LayoutParams.MATCH_PARENT;
        mWindowManager.updateViewLayout(mPreview, mParams);
        mWindowManager.updateViewLayout(mTopView, mParams);

        mIsForeground = true;
    }

    void backgroundDisplay() {
        mParams.width = BG_PREVIEW_WINDOW_WIDTH;
        mParams.height = BG_PREVIEW_WINDOW_HEIGHT;
        mWindowManager.updateViewLayout(mPreview, mParams);
        mWindowManager.updateViewLayout(mTopView, mParams);

        mIsForeground = false;
    }

    boolean inForegroundMode() {
        return mIsForeground;
    }

    private void releaseCamera() {
        if (mCamera != null) {
            mCamera.stopPreview();
            mCamera.release(); // release the camera for other applications
            mCamera = null;
        }
    }

    private Toast mLastToast;
    void showToast(String msg) {
        Toast toast = Toast.makeText(getApplicationContext(), msg, Toast.LENGTH_SHORT);
        toast.show();
        mLastToast = toast;
    }

    void cancelToast() {
        if (mLastToast != null) {
            mLastToast.cancel();
            mLastToast = null;
        }
    }

    static private int mTotalFaces;
    static private int mPreFacesNum;
    static void onFaceDetection(Face[] faces) {
        int lastTotal = mTotalFaces;

        if (faces != null && mPreFacesNum != faces.length) {
            mTotalFaces += faces.length;
            mPreFacesNum = faces.length;
        }

        if (mTotalFaces > lastTotal && FaceDetectService.mThis != null) {
            Message msg = FaceDetectService.mThis.mHandler.obtainMessage(MSG_UPDATE_STATUS, mTotalFaces, faces.length);
            FaceDetectService.mThis.mHandler.sendMessageDelayed(msg, 800);
        }

        if (FaceDetectService.mThis == null || FaceDetectService.mThis.mTopView == null) {
            return;
        }

        Matrix matrix = new Matrix();
        // Need mirror for front camera.
        boolean mirror = (FaceDetectService.mThis.mCameraCurrentlyLocked == 1);
        matrix.setScale(mirror ? -1 : 1, 1);
        // This is the value for android.hardware.Camera.setDisplayOrientation.
        matrix.postRotate(0);
        // Camera driver coordinates range from (-1000, -1000) to (1000, 1000).
        // UI coordinates range from (0, 0) to (width, height).
        int width = FaceDetectService.mThis.mTopView.getWidth();
        int height = FaceDetectService.mThis.mTopView.getHeight();
        matrix.postScale(width / 2000f, height / 2000f);
        matrix.postTranslate(width / 2f, height / 2f);

        RectF[] dstArr = new RectF[faces.length];
        for (int i=0; i<faces.length; i++) {
            dstArr[i] = new RectF();
            matrix.mapRect(dstArr[i], new RectF(faces[i].rect));
        }
        FaceDetectService.mThis.mTopView.mRects = new Rect[dstArr.length];
        for (int i=0; i<dstArr.length; i++) {
            FaceDetectService.mThis.mTopView.mRects[i] = new Rect();
            FaceDetectService.mThis.mTopView.mRects[i].left = (int) (dstArr[i].left + 0.5f);
            FaceDetectService.mThis.mTopView.mRects[i].right = (int) (dstArr[i].right + 0.5f);
            FaceDetectService.mThis.mTopView.mRects[i].top = (int) (dstArr[i].top + 0.5f);
            FaceDetectService.mThis.mTopView.mRects[i].bottom = (int) (dstArr[i].bottom + 0.5f);
        }

        FaceDetectService.mThis.mTopView.invalidate();
    }

    private void updateToAnalytics(int total, int current) {
        Intent intent = new Intent("com.motorola.analytics.action.FACESHOWUP");
        intent.setComponent(new ComponentName("com.motorola.analytics",
                "com.motorola.analytics.DemoModeFaceReceiver"));
        intent.putExtra("SESSION_ID", SESSION_ID);
        intent.putExtra("COUNT", total);
        intent.putExtra("CURRENT_COUNT", current);
        intent.putExtra("MALE", -1);
        intent.putExtra("FEMALE", -1);
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        try {
            getApplicationContext().startActivity(intent);
        } catch (ActivityNotFoundException e) {}
    }

    private static class MyHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            if (msg.what == MSG_UPDATE_STATUS) {
                if (!hasMessages(MSG_UPDATE_STATUS)) {
                    FaceDetectService.mThis.showToast("Total face(s): " + msg.arg1 +
                            ", current face(s): " + msg.arg2);

                    FaceDetectService.mThis.updateToAnalytics(msg.arg1, msg.arg2);
                }
            } else if (msg.what == MSG_GET_PREVIEW) {
                if (FaceDetectService.mThis != null && FaceDetectService.mThis.mPreview != null) {
                    FaceDetectService.mThis.mCamera.setOneShotPreviewCallback(
                            FaceDetectService.mThis.mPreviewCallback);
                }
            }
        }
    }
}

/**
 * A simple wrapper around a Camera and a SurfaceView that renders a centered
 * preview of the Camera to the surface. We need to center the SurfaceView
 * because not all devices have cameras that support preview sizes at the same
 * aspect ratio as the device's display.
 */
class CameraPreview extends ViewGroup implements SurfaceHolder.Callback {
    private final String TAG = "Preview";

    SurfaceView mSurfaceView;
    SurfaceHolder mHolder;
    Size mPreviewSize;
    List<Size> mSupportedPreviewSizes;
    Camera mCamera;

    CameraPreview(Context context) {
        super(context);

        mSurfaceView = new SurfaceView(context);
        addView(mSurfaceView);

        // Install a SurfaceHolder.Callback so we get notified when the
        // underlying surface is created and destroyed.
        mHolder = mSurfaceView.getHolder();
        mHolder.addCallback(this);
        //mHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
    }

    public void setCamera(Camera camera) {
        mCamera = camera;

        if (mCamera != null) {
            mSupportedPreviewSizes = mCamera.getParameters().getSupportedPreviewSizes();

            if (FaceDetectService.mFaceDetectMethod == FaceDetectService.BUILTIN_DETECTOR) {
                int maxNumDetectedFaces = mCamera.getParameters().getMaxNumDetectedFaces();
                if (maxNumDetectedFaces == 0) {
                    FaceDetectService.mFaceDetectMethod = FaceDetectService.CUSTOM_WITH_FACE_DETECTOR;
                }
            }

            if (FaceDetectService.mFaceDetectMethod == FaceDetectService.BUILTIN_DETECTOR) {
                FaceDetectionListener faceDetectionListener = new FaceDetectionListener() {
                    @Override
                    public void onFaceDetection(Face[] faces, Camera camera) {
                        if (faces.length > 0) {
                            ArrayList<Face> faceList = new ArrayList<Face> ();
                            for (Face f : faces) {
                                if (f.score >= 50) {
                                    faceList.add(f);
                                }
                            }
                            if (!faceList.isEmpty()) {
                                faces = new Face[faceList.size()];
                                faceList.toArray(faces);
                            }
                        }

                        FaceDetectService.onFaceDetection(faces);
                    }
                };
                mCamera.setFaceDetectionListener(faceDetectionListener);
            } else if (FaceDetectService.mFaceDetectMethod == FaceDetectService.CUSTOM_WITH_FACE_DETECTOR) {
                mCamera.setOneShotPreviewCallback(FaceDetectService.mThis.mPreviewCallback);
            }

            Camera.Parameters parameters = camera.getParameters();
            parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);

            requestLayout();
        } else {
            mSupportedPreviewSizes = null;
        }
    }

    public void switchCamera(Camera camera) {
        setCamera(camera);
        try {
            camera.setPreviewDisplay(mHolder);
        } catch (IOException exception) {
            Log.e(TAG, "IOException caused by setPreviewDisplay()", exception);
        }
        Camera.Parameters parameters = camera.getParameters();
        parameters.setPreviewSize(mPreviewSize.width, mPreviewSize.height);
        requestLayout();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // We purposely disregard child measurements because act as a
        // wrapper to a SurfaceView that centers the camera preview instead
        // of stretching it.
        final int width = resolveSize(getSuggestedMinimumWidth(),
                widthMeasureSpec);
        final int height = resolveSize(getSuggestedMinimumHeight(),
                heightMeasureSpec);
        setMeasuredDimension(width, height);

        if (mSupportedPreviewSizes != null) {
            mPreviewSize = getOptimalPreviewSize(mSupportedPreviewSizes, width,
                    height);
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (changed && getChildCount() > 0) {
            final View child = getChildAt(0);

            final int width = r - l;
            final int height = b - t;

            int previewWidth = width;
            int previewHeight = height;
            if (mPreviewSize != null) {
                previewWidth = mPreviewSize.width;
                previewHeight = mPreviewSize.height;
            }

            // Center the child SurfaceView within the parent.
            if (width * previewHeight > height * previewWidth) {
                final int scaledChildWidth = previewWidth * height
                        / previewHeight;
                child.layout((width - scaledChildWidth) / 2, 0,
                        (width + scaledChildWidth) / 2, height);
            } else {
                final int scaledChildHeight = previewHeight * width
                        / previewWidth;
                child.layout(0, (height - scaledChildHeight) / 2, width,
                        (height + scaledChildHeight) / 2);
            }
        }
    }

    public void surfaceCreated(SurfaceHolder holder) {
        // The Surface has been created, acquire the camera and tell it where
        // to draw.
        try {
            if (mCamera != null) {
                mCamera.setPreviewDisplay(holder);
            }
        } catch (IOException exception) {
            Log.e(TAG, "IOException caused by setPreviewDisplay()", exception);
        }
    }

    public void surfaceDestroyed(SurfaceHolder holder) {
        // Surface will be destroyed when we return, so stop the preview.
        stopFaceDetection();
        if (mCamera != null) {
            mCamera.stopPreview();
        }
    }

    private Size getOptimalPreviewSize(List<Size> sizes, int w, int h) {
        final double ASPECT_TOLERANCE = 0.1;
        double targetRatio = (double) w / h;
        if (sizes == null)
            return null;

        Size optimalSize = null;
        double minDiff = Double.MAX_VALUE;

        int targetHeight = h;

        // Try to find an size match aspect ratio and size
        for (Size size : sizes) {
            double ratio = (double) size.width / size.height;
            if (Math.abs(ratio - targetRatio) > ASPECT_TOLERANCE)
                continue;
            if (Math.abs(size.height - targetHeight) < minDiff) {
                optimalSize = size;
                minDiff = Math.abs(size.height - targetHeight);
            }
        }

        // Cannot find the one match the aspect ratio, ignore the requirement
        if (optimalSize == null) {
            minDiff = Double.MAX_VALUE;
            for (Size size : sizes) {
                if (Math.abs(size.height - targetHeight) < minDiff) {
                    optimalSize = size;
                    minDiff = Math.abs(size.height - targetHeight);
                }
            }
        }
        return optimalSize;
    }

    public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
        // Now that the size is known, set up the camera parameters and
        // begin the preview.
        if (mCamera != null) {
            Camera.Parameters parameters = mCamera.getParameters();
            parameters.setPreviewSize(mPreviewSize.width, mPreviewSize.height);
            requestLayout();

            mCamera.setParameters(parameters);

            stopFaceDetection();
            mCamera.startPreview();
            startFaceDetection();
        }
    }

    private boolean mFaceDetectRunning;
    void startFaceDetection() {
        if (FaceDetectService.mFaceDetectMethod == FaceDetectService.BUILTIN_DETECTOR) {
            if (false == mFaceDetectRunning && mCamera != null) {
                mCamera.startFaceDetection();
                mFaceDetectRunning = true;
            }
        } else if (FaceDetectService.mFaceDetectMethod == FaceDetectService.CUSTOM_WITH_FACE_DETECTOR) {
        }
    }

    void stopFaceDetection() {
        if (FaceDetectService.mFaceDetectMethod == FaceDetectService.BUILTIN_DETECTOR) {
            if (mFaceDetectRunning) {
                if (mCamera != null) {
                    mCamera.stopFaceDetection();
                }
                mFaceDetectRunning = false;
            }
        } else if (FaceDetectService.mFaceDetectMethod == FaceDetectService.CUSTOM_WITH_FACE_DETECTOR) {
        }
    }

    void setCameraDisplayOrientation() {
        if (FaceDetectService.mThis == null || mCamera == null) {
            return;
        }
        int cameraId = FaceDetectService.mThis.mCameraCurrentlyLocked;
        WindowManager wm = FaceDetectService.mThis.mWindowManager;

        android.hardware.Camera.CameraInfo info = new android.hardware.Camera.CameraInfo();
        android.hardware.Camera.getCameraInfo(cameraId, info);
        int rotation = wm.getDefaultDisplay().getRotation();
        int degrees = 0;
        switch (rotation) {
            case Surface.ROTATION_0: degrees = 0; break;
            case Surface.ROTATION_90: degrees = 90; break;
            case Surface.ROTATION_180: degrees = 180; break;
            case Surface.ROTATION_270: degrees = 270; break;
        }

        int result;
        if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
            result = (info.orientation + degrees) % 360;
            result = (360 - result) % 360;  // compensate the mirror
        } else {  // back-facing
            result = (info.orientation - degrees + 360) % 360;
        }
        mCamera.setDisplayOrientation(result);
    }
}

class DrawOnTop extends View {
    Rect[] mRects;
    private Paint mPaint;

    DrawOnTop(Context context) {
        super(context);

        mPaint = new Paint();
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setColor(Color.GREEN);
        mPaint.setStrokeWidth(2f);

        setOnClickListener( new OnClickListener(){
            @Override
            public void onClick(View v) {
                if (FaceDetectService.mThis != null &&
                        FaceDetectService.mThis.inForegroundMode() == false) {
                    if (v instanceof DrawOnTop) {
                        Intent intent = FaceDetectService.mThis.getPackageManager().getLaunchIntentForPackage("com.motorola.demomode.facedetect");
                        FaceDetectService.mThis.getApplicationContext().startActivity(intent);
                    }
                }
            }
        });
        setClickable(true);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        if (mRects != null && FaceDetectService.mThis != null &&
                FaceDetectService.mThis.inForegroundMode()) {
            for (Rect r : mRects) {
                canvas.drawRect(r, mPaint);
            }
        }
        super.onDraw(canvas);
    }
}
